package east;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import robocode.AdvancedRobot;
import robocode.DeathEvent;
import robocode.RobotDeathEvent;
import robocode.ScannedRobotEvent;
import robocode.WinEvent;
import robocode.util.Utils;

public class Scanner
{
	
	/**
	 * The higher this is, the less likely we are to loose lock, but the
	 * less efficient we are.
	 */
	public double SENSITIVITY = 0.15;
	
	public int LOCK_THRESHOLD = 9;
	
	private HashMap<String, EnemyInfo> scanInfo;
	private AdvancedRobot r;
	private double scanDir;
	private double CWAngle = -1;
	private double CCWAngle = -1;
	
	private boolean anotherRobotHasFired = false;
	
	private ScannerStatistics statistics;
	
	
	public Scanner(AdvancedRobot robot)
	{
	    this.r = robot;
	    this.scanInfo = new HashMap<String, EnemyInfo>();
	    robot.setAdjustGunForRobotTurn(true);
	    robot.setAdjustRadarForGunTurn(true);
	    robot.setAdjustRadarForRobotTurn(true);
		scanDir = Double.POSITIVE_INFINITY;
		
		statistics = new ScannerStatistics(r.getOthers());
	}
	
	public boolean hasEnemyFired()
	{
		boolean temp = this.anotherRobotHasFired;
		this.anotherRobotHasFired = false;
		return temp;
	}
	
	/**
	 * Return closest enemy within minD
	 * @param minD
	 * @return
	 */
	public EnemyInfo getClosest(double minD)
	{
		double minDistance = minD;
		EnemyInfo closestEnemy = null;
		for(EnemyInfo enemy : scanInfo.values())
		{
			double d = enemy.getDistanceTo(r.getX(), r.getY());
			if (d < minDistance)
			{
				minDistance = d; closestEnemy = enemy;
			}
		}
		return closestEnemy;
	}
	
	public void move()
	{
		boolean lostLock = false;
		for(EnemyInfo enemy : scanInfo.values())
		{
			enemy.update();
			long time = r.getTime() - enemy.scanTime;
			statistics.add(time, r.getOthers());
			if (time > LOCK_THRESHOLD) lostLock = true;
			
			if (enemy.deltaE < 3 && enemy.deltaE > 0.1)
			{
				this.anotherRobotHasFired = true;
			}
		}
		if (lostLock)
		{
			//System.out.print("Lost Lock! ");
			//System.out.println(r.getTime());
			if(scanDir < 0)
			{
				CCWAngle = Utils.normalAbsoluteAngle(r.getHeadingRadians() + Math.PI / 2);
			}
			else
			{
				CWAngle = Utils.normalAbsoluteAngle(r.getHeadingRadians() - Math.PI / 2);
			}
		}
		else if (allBotsFound())
		{
			checkReverseRadar();
		}

		
		r.setTurnRadarRight(scanDir);
		r.scan();
	}
	
	
	private boolean allBotsFound()
	{
		return scanInfo.size() == r.getOthers();
	}
	
	private void reverseDirection()
	{
		scanDir = - scanDir;
	}
	
	private void checkReverseRadar()
	{		
		//System.out.print("Radar Heading: ");
		//System.out.println(r.getRadarHeadingRadians());
		//System.out.println(untilCW);
		//System.out.println(untilCCW);
		
		if(scanDir > 0 && 
		   Utils.normalRelativeAngle(r.getRadarHeadingRadians() - CWAngle) > 0 &&
		   Utils.normalRelativeAngle(r.getRadarHeadingRadians() - Math.PI/2 - CWAngle) < 0 ||
		   scanDir < 0 &&
		   Utils.normalRelativeAngle(r.getRadarHeadingRadians() - CCWAngle) < 0 &&
		   Utils.normalRelativeAngle(r.getRadarHeadingRadians() + Math.PI/2 - CCWAngle) > 0)
		{
			reverseDirection();
			findAndSetAngles();
		}
	}

	
	public void onScan(ScannedRobotEvent e)
	{
		///////////////////// UPDATE ENEMY INFORMATION ////////////////
		
		String name = e.getName();
		EnemyInfo enemy = scanInfo.get(name);
		boolean findAndSetAngles = false;
		if (enemy == null)
		{
			scanInfo.put(name, enemy = new EnemyInfo());
			
			// If the bot we just found was the last bot to find
			if (allBotsFound())
			{
				findAndSetAngles = true;	
			}
		}

		enemy.scanTime = r.getTime();
		double absDir = r.getHeadingRadians() + e.getBearingRadians();
		
		enemy.x = r.getX() + Math.sin(absDir) * e.getDistance();
		enemy.y = r.getY() + Math.cos(absDir) * e.getDistance();
		
		enemy.vx = Math.sin(absDir) * e.getVelocity();
		enemy.vy = Math.cos(absDir) * e.getVelocity();
		
		if (enemy.E != -1)
		{
			enemy.deltaE = enemy.E - e.getEnergy();
		}
		enemy.E = e.getEnergy();
		
		enemy.lastHeading = enemy.heading;
		enemy.heading = e.getHeadingRadians();
		
		if (findAndSetAngles) findAndSetAngles();
	}
	
	private void findAndSetAngles()
	{	
		// we've already won!  don't do anything stupid.  just return.
		if(scanInfo.size() == 0)
		{
			return;
		}
		
		for(EnemyInfo anenemy : scanInfo.values())
		{
			anenemy.update();
			anenemy.setAngleTo(r.getX(), r.getY());
		}
		EnemyInfo[] info = new EnemyInfo[scanInfo.size()]; 
		info = scanInfo.values().toArray(info);
		Arrays.sort(info);
		int i = maxAngleDifferenceIndex(info);
		
		//System.out.print("CWAngle: ");
		//System.out.println(info[i].angleTo);
		//System.out.print("CCWAngle: ");
		//System.out.println(info[(i+1) % info.length].angleTo);
		
		CWAngle = info[i].angleTo + SENSITIVITY;
		CCWAngle = info[(i+1) % info.length].angleTo - SENSITIVITY;
		
	}

	public void onRobotDeath(RobotDeathEvent e)
	{
		scanInfo.remove(e.getName());		
	}
	
	public void onDeath(DeathEvent e)
	{
		System.out.println(statistics);
	}
	
	public void onWin(WinEvent e)
	{
		System.out.println(statistics);
	}
	
	public static class EnemyInfo implements Comparable<Object>
	{
		public double vy;
		public double vx;
		public double y;
		public double x;
		public double heading=0;
		public double lastHeading=0;
		
		/**
		 * Previous energy - Current energy
		 */
		public double deltaE;
		
		/**
		 * Current energy
		 */
		public double E=-1;
		public long scanTime;
		
		protected double angleTo;
		
		public void setAngleTo(double xFrom, double yFrom)
		{
			angleTo = Utils.normalAbsoluteAngle(Math.atan2(x - xFrom, y - yFrom));
		}
		
		public double getDistanceTo(double xFrom, double yFrom)
		{
			return distance(xFrom, this.x, yFrom, this.y);
		}
		
		public void update()
		{
			y += vy;
			x += vx;
		}

		public int compareTo(Object o)
		{
			return Double.compare(angleTo, ((EnemyInfo) o).angleTo);
		}

		@Override
		public String toString()
		{
			StringBuilder builder = new StringBuilder();
			builder.append("EnemyInfo [scanTime=").append(scanTime).append("]");
			return builder.toString();
		}
		
	}
	
	private class ScannerStatistics
	{

		private ArrayList<ArrayList<Integer>>stats;
		private int max = 0;
		
		public ScannerStatistics(int maxBots)
		{
			stats = new ArrayList<ArrayList<Integer>>(maxBots+1);
			stats.add(new ArrayList<Integer>(10));
			for (int i = 1; i <= maxBots; ++i)
			{
				stats.add(new ArrayList<Integer>(1000));
			}
		}
		
		public void add(long l, int botsLeft)
		{
			stats.get(botsLeft).add((int) l);
		}
		
		@Override
		public String toString()
		{
			StringBuilder builder = new StringBuilder();
			builder.append("Scanning Statistics: Mean +/- StDev, Max\n");
			double mean; double stdev;
			for(int i = stats.size()-1; i > 0; --i)
			{
				mean = avg(stats.get(i));
				stdev = stdev(stats.get(i), mean);
				builder.append(i).append(" Bots: ").append(mean);
				builder.append(" +/- ").append(stdev);
				builder.append(", ").append(max).append('\n');
				max = 0;
			}
			
			return builder.toString();
		}
		
		private double avg(ArrayList<Integer> d)
		{
			double tot = 0;
            for (int j = 0; j < d.size(); j++) {
                tot += d.get(j);
            }
            return tot/d.size();
		}
		
		private double stdev(ArrayList<Integer> d, double mean)
		{
			double tot = 0; double temp; int temp2;
            for (int j = 0; j < d.size(); j++) 
            {
            	temp = mean - d.get(j);
            	tot += temp * temp;	
            	temp2 = d.get(j);
            	if (temp2 > max) max = temp2;
            }
            return Math.sqrt(tot/d.size());
		}
	}	
	
	private static int maxAngleDifferenceIndex(EnemyInfo[] info)
	{
		int maxindex = -1; double max = Double.MIN_VALUE;
		
		double temp;
		for(int i=0; i < info.length-1; i++)
		{
			temp = info[i+1].angleTo - info[i].angleTo;
			if (temp > max)
			{
				max = temp;
				maxindex = i;
			}
		}
		temp = info[0].angleTo - info[info.length-1].angleTo + 2 * Math.PI;
		if (temp > max)
		{
			max = temp;
			maxindex = info.length-1;
		}
		return maxindex;
	}
	
	private static double distance(double x1, double y1, double x2, double y2)
	{
		double x = x2-x1; double y = y2-y1; return Math.sqrt(x*x+y*y);
	}

}
